Add multi-profile authentication support#293
Conversation
8e7745e to
24c32c4
Compare
Displays the email and workspace for the active token, with proper error messages for unauthenticated and invalid-token cases.
sergey-borovkov
left a comment
There was a problem hiding this comment.
Review — Multi-profile authentication support
Overview
Replaces the plain-text ~/.screenly single-token file with a YAML profile store, adding login --name, logout --name, auth list, auth switch, and me commands. Plain-text legacy files are transparently migrated. The migration design (auto-upgrade plain text → default profile on first write) is well thought out, and there's solid unit-test coverage for Authentication state transitions.
Correctness
-
Token file permissions are not restricted. The YAML file now holds multiple tokens but is still written via
fs::writewith default umask. The old behavior had the same flaw, but the blast radius is larger now (compromise of one file = all profiles). Considerfs::set_permissions(&path, Permissions::from_mode(0o600))on Unix afterwrite_store(src/authentication.rswrite_store). -
remove_tokenpost-delete active selection is non-deterministic. When removing the active profile,store.active = store.tokens.keys().next().cloned()picks an arbitraryHashMapkey — different on each run. Sort or pick deterministically (e.g., alphabetical first), andinfo!()the new active profile name so the user knows what just happened. -
Logoutpanics on any error via.expect(\"Failed to remove token.\")incli.rs. Pre-existing pattern, but the newremove_tokenhas more failure modes (NoCredentials, ProfileNotFound, Yaml parse). Better to print a friendly message andexit(1), matching other auth paths. -
fetch_profile_infoassumes/v4.1/users/mereturns an array.user.get(0).and_then(|u| u[\"email\"].as_str())only works if the response is[{...}], not{...}. The tests mock both shapes consistently, but please confirm the live API actually returns an array — if it returns an object,mewill silently print\"unknown\". -
No User-Agent header on profile-info requests. Existing
Authentication::client()setsscreenly-cli {version};fetch_profile_infobuilds a bare client. Minor, but inconsistent. -
Race on store updates.
read → mutate → writeinverify_and_store_token/remove_token/switch_profileisn't atomic; two concurrentscreenly logininvocations can lose writes. CLI use makes this unlikely, but a temp-file + rename would be cheap insurance.
Style / Conventions
-
Duplicated table-rendering block in
AuthCommands::ListandAuthCommands::Switch { name: None }— ~25 identical lines. Extract aprint_profiles_table(profiles, api_url)helper. -
AuthCommands::Switchwith nonameprinting the list is undocumented. The clap help reads<NAME> — Profile name to activate, with no hint that omitting it is a meaningful action. Either document it (/// Without an argument, prints the profile list.) or makenamerequired. -
Me"Profile:" line shown conditionally based onactive_profile_name(). WhenAPI_TOKENenv var is in use, the field is silently omitted in both human and JSON output. Reasonable, but consider printingProfile: (from API_TOKEN env)so the user understands why no profile name appears. -
Redundant
use serde_yaml;at the top ofsrc/authentication.rs— theserde_yaml::Errorreference in the enum already pulls in the crate path. -
#[error(\"yaml error\")]swallows the underlying message. Use#[error(\"yaml error: {0}\")]so parse failures are diagnosable.
Test coverage
Good additions for migration, switch, list, and fetch_profile_info. Missing:
loginrequires--namewhen multiple profiles exist — the new safety check is the headline UX claim of the PR and isn't covered.verify_and_store_tokenpreserves existing profiles when called with a newname— easy to regress if someone refactors toTokenStore::default().remove_token(Some(name))with explicit name — only theNone(active) path is tested.- Remove-active-profile picks a remaining one as new active — would also catch the non-determinism above.
Performance
auth listandauth switch(no arg) make 2 sequential HTTP requests per profile (users/me+teams). For 5+ profiles this is a noticeable hang. Consider parallelizing withfutures::join_all(already a dep), or calling a combined endpoint if available.
Security
- See file-permissions point above — primary concern.
- Tokens are passed around in
(name, token, is_active)tuples returned fromlist_profiles(). Internal-only, but token strings are unused in the caller — consider returning(name, is_active)and exposing token lookup via a separate method to reduce accidental-print risk.
Summary
Solid feature with a clean migration path. Top priorities before merge: deterministic active-profile selection on removal, file permissions (0o600), graceful logout error handling, and tests for the --name enforcement and explicit-name removal. The duplicate table code and undocumented switch no-arg behavior are easy follow-ups.
Summary
Replaces the single-token
~/.screenlyfile with a YAML-based profile store,enabling multiple named authentication profiles with a single active profile at a time.
screenly login --name <profile>stores a token under a named profile and switches to it.Defaults to
"default"on a fresh install. Requires--namewhen other profiles alreadyexist to prevent accidental overwrites.
screenly logout --name <profile>removes a specific profile. Omitting--nameremovesthe active profile.
screenly auth listshows all stored profiles in a table with email and workspace namefetched from the API, with
*marking the active profile.screenly auth switch <name>changes the active profile. Called without an argument,it prints the profile list as a hint.
screenly meshows the email and workspace for the currently active token. Accepts--json. Returns a clear error if not logged in or if the token is invalid.API_TOKENenvironment variable still overrides everything.~/.screenlyfiles (original format) are transparently migrated on first write.